#include "mesh.h"

/**
 * 1.4.12 - Steven Hartin - Added bounding data into the displayable object.
 *
 * 17.3.12 - Fixed textures not being referenced properly
 */

CMesh::CMesh():
            m_pMesh(nullptr), 
            m_dwNumMaterials(0)
{
    /**
     * Constructor. sets all variables to to 0 and pointers to nullptrs
     */
}

void CMesh::setMaterial(CMaterial Material, DWORD dwSubset)
{
    /**
     * Adds a material to material array and set its subset. 
     *
     * @param CMaterial pMaterial The material to add to the material list 
     * @param DWORD dwSubset The subset to apply the material to. 
     */

    map<DWORD, CMaterial>::iterator pMaterialPos(
                                        m_arrMeshMaterials.find(dwSubset));
    if (pMaterialPos != m_arrMeshMaterials.end())
    {
        m_arrMeshMaterials.erase(pMaterialPos);
    }
    m_arrMeshMaterials.insert(pair<DWORD,CMaterial>(dwSubset, Material));
}

void CMesh::setTexture(CTexture* texture, DWORD dwSubset){
    /** 
     * Adds a texture to texture array and set its subset.
     *
     * @param CTexture* texture The texture to add to the texture array.
     * @param DWORD dwSubset The subset to apply the texture to. 
     */
    map<DWORD, CTexture*>::iterator pTexture(m_arrMeshTextures.find(dwSubset));
    if (pTexture != m_arrMeshTextures.end()) 
    {
        m_arrMeshTextures.erase(pTexture);
    }
    m_arrMeshTextures.insert(pair<DWORD,CTexture*>(dwSubset, texture));
}

CMesh::~CMesh()
{
    /**
     * Destructor, ensure that the mesh objects are released to memory, and
     * all texture references are decremented.
     */

    // Release the Mesh.
    if (m_pMesh != nullptr)
    {
        m_pMesh->Release();
        m_pMesh = nullptr;
    }
}

HRESULT CMesh::loadMesh(LPDIRECT3DDEVICE9 device, LPCTSTR filename)
{
    /**
     * Attempts to load a mesh from a given X file name. In turn, this will try
     * to load any textures which have not yet been loaded into memory,
     * and increment all reference counters.
     * 
     * @param LPDIRECT3DDEVICE9 device A reference to the DirectX device
     * @param LPCTSTR filename The file to load out of
     * @return HRESULT The success of the mesh being loaded from file.
     */

    //local variable to temporarily hold data extracted from the .x file 
    LPD3DXBUFFER pD3DXMtrlBuffer; 

    // Load the mesh from the specified file
    if (FAILED(D3DXLoadMeshFromX(filename, D3DXMESH_SYSTEMMEM,
                                 device,
                                 NULL,
                                 &pD3DXMtrlBuffer,
                                 NULL,
                                 &m_dwNumMaterials,
                                 &m_pMesh)))
    {
        m_pMesh = nullptr;
        MessageBox(NULL, (string("Cannot find X file: ") + filename).c_str(), 
                   "Mesh::LoadMesh", MB_OK);
        return E_FAIL;
    }

    // Extract the material properties and texture names from the 
    // pD3DXMtrlBuffer
    D3DXMATERIAL *d3dxMaterials = 
                 (D3DXMATERIAL*)pD3DXMtrlBuffer->GetBufferPointer();

    // For each sub-mesh
    for (DWORD i(0); i < m_dwNumMaterials; ++i)
    {
        // Copy the material        
        CMaterial material(d3dxMaterials[i].MatD3D);

        // Set the ambient color for the material (D3DX does not do this)
        material.setAmbient(material.getDiffuse());

        // If there is a filename for the texture for that part of the mesh
        CTexture* pTexture(nullptr);
        
        if (d3dxMaterials[i].pTextureFilename != NULL &&
            lstrlen(d3dxMaterials[i].pTextureFilename) > 0 )
        {
            // Create the texture
            string szPath(d3dxMaterials[i].pTextureFilename);

            // *TODO* Check all texture directories
            szPath = string("textures/tiles/") + szPath;
            CTextureContainer *pTextures(CTextureContainer::getInstance());
            if (pTextures->getTexture(szPath) != nullptr)
            {
                // Texture is already loaded into memory, increment the
                // reference counter to this texture.
                pTexture = pTextures->getTexture(szPath);
            }
            else
            {
                pTexture = pTextures->addTexture(szPath, szPath, device);
            }
        }

        setMaterial(material, i);
        setTexture(pTexture, i);
    }

    // Done with the material buffer
    pD3DXMtrlBuffer->Release();

    _fetchVertexData();
    _calculateBoundingBox();
    _calculateBoundingSphere();

    return S_OK;
}

void CMesh::loadMesh(LPD3DXMESH pMesh)
{
    /**
     * Loads a mesh from a given mesh object. The only thing this does is
     * keep a reference to the mesh
     */
    m_pMesh = pMesh;
    m_dwNumMaterials = 1;
}

void CMesh::render(DWORD nSubset, LPDIRECT3DDEVICE9 pDevice) const
{
    /**
     * Renders a given subset. Assumes that textures / materials have already
     * been assigned.
     *
     * @param DWORD nSubset The subset to render
     * @param LPDIRECT3DDEVICE9 pDevice The device to render to.
     */
    m_pMesh->DrawSubset(nSubset);
}

const DWORD CMesh::getSubsetCount() const
{
    /**
     * @return const DWORD The amount of subsets this mesh contains.
     */
    return m_dwNumMaterials;
}

bool CMesh::hasMaterial(DWORD nSubset) const
{
    /**
     * @param DWORD nSubset The subset to test for material validity.
     * @return bool Whether or not a mesh has a particular material for a given
     *      subset.
     */
    return m_arrMeshMaterials.find(nSubset) != m_arrMeshMaterials.end();
}

const CMaterial& CMesh::getMaterial(DWORD nSubset) const
{
    /**
     * @param DWORD nSubset The subset of the mesh to obtain the material.
     * @return const CMaterial& A reference to the material value for the subset
     * @throw runtime_error If the subset does not contain a material.
     */
    if (! hasMaterial(nSubset))
    {
        stringstream sstream;
        sstream << "CMesh::getMaterial() - "
                << "Cannot obtain material for subset "
                << nSubset
                << " for mesh object.";
        throw std::runtime_error(sstream.str());
    }
    return m_arrMeshMaterials.find(nSubset)->second;
}

bool CMesh::hasTexture(DWORD nSubset) const
{
    /**
     * @param DWORD nSubset The subset of the mesh to test for texture validity
     * @return bool Whether or not the subset has a texture for this mesh.
     */
    return (m_arrMeshTextures.find(nSubset) != m_arrMeshTextures.end() &&
            m_arrMeshTextures.find(nSubset)->second != nullptr);
}
CTexture *CMesh::getTexture(DWORD nSubset) const
{
    /**
     * @param DWORD nSubset The subset of the mesh to obtain the texture for.
     * @return CTexture* A pointer to the texture which exists at the subset.
     * @throw runtime_error If the subset does not contain a texture.
     */
    if (! hasMaterial(nSubset))
    {
        stringstream sstream;
        sstream << "CMesh::getTexture() - "
                << "Cannot obtain texture for subset "
                << nSubset
                << " for mesh object.";
        throw std::runtime_error(sstream.str());
    }
    return m_arrMeshTextures.find(nSubset)->second;
}

void CMesh::_fetchVertexData()
{
    /**
     * Fetches the vertex data of the mesh. This will contain the plane data
     * for every face on the mesh.
     */
    if (m_pMesh != nullptr)
    {
        // We can only fetch data if we have populated the mesh pointer

        if (m_arrVertices.size() > 0)
        {
            // We have already obtained the vertex data
            return;
        }

        BYTE *pVertexData;
        HRESULT hr = m_pMesh->LockVertexBuffer(D3DLOCK_READONLY, 
                                             (void**)&pVertexData);
        if(FAILED(hr))
        {
            // Cannot lock vertex buffer.
            return;
        }

        DWORD nVertexCount(m_pMesh->GetNumVertices());
        LPDIRECT3DINDEXBUFFER9 pIndexBufferData;
        BYTE *pIndexBuffer;
        hr = m_pMesh->LockIndexBuffer(D3DLOCK_READONLY, 
                                             (void**)&pIndexBuffer);
        if(FAILED(hr))
        {
            // Cannot lock index buffer.
           return;
        }

        D3DINDEXBUFFER_DESC indexDescription;
        m_pMesh->GetIndexBuffer(&pIndexBufferData);
        pIndexBufferData->GetDesc(&indexDescription);        

        DWORD nVertexSize(m_pMesh->GetNumBytesPerVertex());
        DWORD nFaceCount(m_pMesh->GetNumFaces());

        m_arrVertices.reserve(nVertexCount);

        // Resize array to hold enough data for the entire vector data
        m_arrPlaneData.reserve(nFaceCount);

        for (DWORD nCurrentIndex(nVertexCount); nCurrentIndex; --nCurrentIndex)
        {
            m_arrVertices.push_back((D3DXVECTOR3*)pVertexData);
            pVertexData += nVertexSize;
        }

        for(DWORD nCurrentFace(nFaceCount); nCurrentFace; --nCurrentFace)
        {
            // Get the position (at the start of the vertex struct)
            TTriangle stPlaneData;
            DWORD nIndex1(0), nIndex2(0), nIndex3(0);
           
            if (indexDescription.Format == D3DFMT_INDEX16)
            {
                nIndex1 = (*(reinterpret_cast<WORD*>(pIndexBuffer)));
                nIndex2 = (*(reinterpret_cast<WORD*>(pIndexBuffer + 
                                                     sizeof(WORD))));
                nIndex3 = (*(reinterpret_cast<WORD*>(pIndexBuffer + 
                                                     sizeof(WORD) * 2)));
                pIndexBuffer += sizeof(WORD) * 3;
            }
            else
            {
                nIndex1 = (*(reinterpret_cast<DWORD*>(pIndexBuffer)));
                nIndex2 = (*(reinterpret_cast<DWORD*>(pIndexBuffer + 
                                                     sizeof(DWORD))));
                nIndex3 = (*(reinterpret_cast<DWORD*>(pIndexBuffer + 
                                                     sizeof(DWORD) * 2)));
                pIndexBuffer += sizeof(DWORD) * 3;
            }
            stPlaneData.vecarrVertices[0] = m_arrVertices[nIndex1];
            stPlaneData.vecarrVertices[1] = m_arrVertices[nIndex2];
            stPlaneData.vecarrVertices[2] = m_arrVertices[nIndex3];

            // Find the face normal, it's ((vector 01) x (vector 02)).
            D3DXVECTOR3 vec01 = (*(stPlaneData.vecarrVertices[1])) - 
                                (*(stPlaneData.vecarrVertices[0]));
            D3DXVECTOR3 vec02 = (*(stPlaneData.vecarrVertices[2])) -
                                (*(stPlaneData.vecarrVertices[0]));

            D3DXVec3Cross(&stPlaneData.vecNormal, &vec01, &vec02);
            D3DXVec3Normalize(&stPlaneData.vecNormal, &stPlaneData.vecNormal);

            // Memberwise copy on the struct, we needn't worry about the
            // expensive cycles as this will be done at load time, not every
            // frame.
            m_arrPlaneData.push_back(stPlaneData);
        }

        m_pMesh->UnlockVertexBuffer();
        m_pMesh->UnlockIndexBuffer();
    }
}

void CMesh::_calculateBoundingBox()
{
    /**
     * Recaulculates the bounding box of the object. The bounding box is
     * guaranteed to contain all of the mesh's points within, and creates an
     * axis-aligned box around the mesh.
     */
    BYTE *pVertexData;
    DWORD nVertexCount(m_pMesh->GetNumVertices());
    DWORD nVertexSize(m_pMesh->GetNumBytesPerVertex());

    HRESULT hr = m_pMesh->LockVertexBuffer(D3DLOCK_READONLY, 
                                             (void**)&pVertexData);
    
    D3DXComputeBoundingBox((D3DXVECTOR3*)pVertexData, nVertexCount, nVertexSize,
                           &m_Shape.vecMinBound, &m_Shape.vecMaxBound);

    m_pMesh->UnlockVertexBuffer();
}

void CMesh::_calculateBoundingSphere()
{
    /** 
     * Recalculates the bounding sphere of the object. The bounding sphere
     * is guaranteed to contain all of the mesh's points within, and creates a
     * 'bubble' around the mesh.
     */

    // Initialise default center of object to origin.
    m_Shape.vecSphereCentre = D3DXVECTOR3(0.0f, 0.0f, 0.0f);    

    for (vector<D3DXVECTOR3*>::const_iterator pVertex(m_arrVertices.begin());
         pVertex != m_arrVertices.end(); ++pVertex)
    {
        // For every point, add the vector to the current centre. This gets the
        // total vector position.
        m_Shape.vecSphereCentre += **pVertex;
    }

    // Divide by the amount of vertices, also make sure we do not divide by 0
    // Stop the world from imploding. This obtains the real origin of the
    // object by getting the average point.
    m_Shape.vecSphereCentre /= m_arrVertices.size() ? 
                               static_cast<float>(m_arrVertices.size()) : 1.0f;
 
    // Default radius to 0
    m_Shape.fRadius = 0.0f;

    for (vector<D3DXVECTOR3*>::const_iterator pVertex(m_arrVertices.begin());
         pVertex != m_arrVertices.end(); ++pVertex)
    {
        // Loop through all vertices in the object, and check if the distance
        // squared of the vector is greater than the current radius, if so,
        // reassign to the new value. By the time the loop finished, the
        // sphere's radius will hold the greatest distance squared. This means
        // that the vertex with the furthest vertex will be inside the radius.
        D3DXVECTOR3 vectorToCenter(**pVertex - m_Shape.vecSphereCentre);
        float distSq(D3DXVec3LengthSq(&vectorToCenter));

        if (distSq > m_Shape.fRadius)
        {
            // We have found a vertex further away from teh center.
            m_Shape.fRadius = distSq;
        }
    }

    // Since we were working with squared numbers, we can finally perform the
    // expensive operation to square root the radius to obtain the actual
    // distance.
    m_Shape.fRadius = sqrtf(m_Shape.fRadius);
}

const TBoundingShape &CMesh::getBoundingShape() const
{
    /**
     * @return const TBoundingShape& The bounding shape data for this mesh. This
     *  is the untransformed data, so it must first be transformed to the same
     *  transformations of the displayable object before it can be accurate.
     */
    return m_Shape;
}